import time
import math
import collections
import os
import json
import jieba
from operator import itemgetter

# 标题层级类型
title_type1 = [("第", "章"), ("第", "条"), ("（", "）")]  # 没有“节”
title_type2 = [("第", "章"), ("第", "节"), ("第", "条")]  # 有“节”
title_type3 = [("第", "章"), ("第", "节"), ("第", "条"), ("（", "）"), ("", ".")]  # 通用

CN_NUM = {'〇': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '零': 0,
          '壹': 1, '贰': 2, '叁': 3, '肆': 4, '伍': 5, '陆': 6, '柒': 7, '捌': 8, '玖': 9, '貮': 2, '两': 2}
CN_UNIT = {'十': 10, '拾': 10, '百': 100, '佰': 100, '千': 1000, '仟': 1000,
           '万': 10000, '萬': 10000, '亿': 100000000, '億': 100000000, '兆': 1000000000000}


# 解析中文数字（无法解析返回-1）
def chinese_to_arabic(cn: str) -> int:
    if len(set(cn) & set(CN_UNIT.keys())) == 0:  # 没有单位的数字，如“一一九”
        res = 0
        for digit in cn:
            if digit in CN_NUM:
                res += 10 * res + CN_NUM[digit]
            else:
                return -1
        return res
    # 有单位的数字，如“一百一十九”
    unit = 0  # current
    ldig = []  # digest
    for cndig in reversed(cn):
        if cndig in CN_UNIT:
            unit = CN_UNIT.get(cndig)
            if unit == 10000 or unit == 100000000:
                ldig.append(unit)
                unit = 1
        elif cndig in CN_NUM:
            dig = CN_NUM.get(cndig)
            if unit:
                dig *= unit
                unit = 0
            ldig.append(dig)
        else:
            return -1
    if unit == 10:
        ldig.append(10)
    val, tmp = 0, 0
    for x in reversed(ldig):
        if x == 10000 or x == 100000000:
            val += tmp * x
            tmp = 0
        else:
            tmp += x
    val += tmp
    return val


# 在文本中寻找标题
# 输入：文本，标题层级类型
# 输出：标题的层级（例如“第一章”1，“第一条”2，“（一）”3，无标题返回-1）、层级内序号（无标题返回-1）、左标志起点和右标志末尾位置（无标题返回-1）
def find_head(text, title_type=None):
    if title_type is None:
        title_type = title_type3
    # 尝试寻找各个层级的标题
    for rank, title in enumerate(title_type):
        # 寻找左标志的位置
        left_i = text.find(title[0])
        if left_i > -1:  # 有左标志，再找右标志
            right_i = text[left_i:].find(title[1])  # 得到右标志相对左标志起点的偏移
            if right_i > -1:  # 有右标志
                num_str = text[left_i + len(title[0]):left_i + right_i]  # 取出左右标志中间的数字字符
                if num_str.isdecimal():  # 阿拉伯数字
                    num = int(num_str)
                else:  # 中文数字
                    # 尝试将左右标志之间的中文数字转化为阿拉伯数字
                    num = chinese_to_arabic(num_str)
                if num > -1:
                    return rank + 1, num, left_i, left_i + right_i + len(title[1])
    return -1, -1, -1, -1


def test_find_head():
    print(find_head('第二百一十六条', title_type1))
    print(find_head('第三百条', title_type1))
    print(find_head('第四百七十条', title_type1))
    print(find_head('（五）', title_type1))
    print(find_head('第二章', title_type2))
    print(find_head('第九十八节', title_type2))
    print(find_head('第一百五十六条', title_type2))
    print(find_head('（五）', title_type2))


# 中文停用词集合
with open('中文停用词表.txt', encoding='utf-8') as f_stop_words:
    stop_words = set(f_stop_words.read().splitlines())

# 分词结果字典（文本→分词列表，避免对同一文本重复分词）
word_cut_dict = {'': []}
# 分词IDF字典（分词→IDF值，用所有叶节点计算全局IDF值）
word_idf_dict = collections.Counter()
# 分词集合
word_set = set()


# 获取去掉停用词的分词列表
def word_cut(text, is_head=False):
    if not is_head or text not in word_cut_dict:  # 不是标题文本或不在字典中
        # 执行分词并将分词添加进集合
        words = [word for word in jieba.cut(text) if word not in stop_words]
        word_set.update(words)
        if is_head:  # 将标题文本分词结果保存进字典
            word_cut_dict[text] = words
        return words
    return word_cut_dict[text]


# 询问时匹配节点TF-IDF值应超过最大值的比例
MATCH_ACCU = 0.75
# 做题时匹配节点TF-IDF值应超过最大值的比例
PROBLEM_ACCU = 0.5
# 匹配节点TF-IDF最低值
TFIDF_MIN = 0.1
# 正文分词的固定词频
CONTENT_FQ = 0.2


# 计算各分词的全局IDF值（提供全部叶节点，在全部法规读取完毕后执行）
def calculate_idf(nodes):
    for word in word_set:
        # 出现该词的节点数量
        Dw = len([1 for node in nodes if node.counter[word] or node.head_counter[word]])
        # 该分词没出现过或在所有节点都出现了，IDF值设为0
        word_idf_dict[word] = 0 if Dw in (0, len(nodes)) else math.log(len(nodes) / Dw, 10)


# 给定一组分词，计算其对于各节点的TF-IDF值
def get_tfidf(q_words, nodes):
    # 如果节点集合为父子（一个父节点与若干子节点），父节点只允许用本级的标题和正文
    is_family = False
    ID_set = set(map(lambda x: x.ID, nodes))
    for node in nodes:
        if ID_set - {node.ID} == set(node.children):  # 节点集中除了自己以外的节点都是自己的子节点
            is_family = True
            break
    if is_family:
        tfidfs = []
        for node in nodes:
            # self_word_set = set(word_cut(node.text) + word_cut(node.content))  # 本级标题和正文的分词集合
            tfidfs.append(sum(((1 if word in node.text else 0) + (1 if word in node.content else 0))
                              * word_idf_dict[word] for word in q_words))
        return tfidfs
    return [sum((node.head_counter[word] + (CONTENT_FQ if node.counter[word] else 0)) * word_idf_dict[word]
                for word in q_words) for node in nodes]


# 标题节点类
class HeadNode:
    def __init__(self, node_id, head, rank, text, content, father_id):
        self.ID = node_id
        self.head = head  # 标题字眼，如”第一章“
        self.rank = rank  # 标题层级，如”章“为1，”节“为2，”条“为3
        # 如果标题文本包含“（图x）”字样，要去掉
        if text.rfind('（') < text.rfind('图') < text.rfind('）'):
            text = text[:text.rfind('（')]
        self.text = text.replace(' ', '')  # 标题文本，如”总 则“，去空格
        self.content = content  # 直属该标题的正文内容
        self.father_ID = father_id  # 上级标题ID
        self.children = []  # 直接下级ID列表
        # 分词计数器初始化
        head_words = word_cut(self.text, True)  # 标题文本分词
        self.head_counter = collections.Counter(head_words)  # 标题文本分词计数器
        self.counter = self.head_counter.copy()  # 通用分词计数器
        # 将标题文本分词计数器转换为词频
        for word in self.head_counter:
            self.head_counter[word] /= len(head_words)

    # 返回去掉括号备注（若有）的标题文本
    def pure_text(self):
        return self.text[:self.text.find('（')] if self.text.endswith('）') else self.text

    # 添加正文内容
    def add_content(self, text):
        # 如果此前内容末尾不是句子结尾符号，则补充一个空格
        if self.content and self.content[-1] not in '：；。！？…':
            self.content += ' '
        self.content += text

    # 正文读取完毕后执行该方法（提供祖辈节点列表）
    def convert_counter(self, father_nodes):
        # 将正文分词添加进通用分词计数器，并转换为词频
        self.counter.update(word_cut(self.content))
        word_num = sum(self.counter.values())  # 文本总词数
        for word in self.counter:
            self.counter[word] /= word_num
        # 汇总所有上级标题文本分词，将词频添加进标题文本分词计数器
        father_head_words = []
        # 添加该祖辈标题文本分词
        for father_node in father_nodes:
            father_head_words += word_cut(father_node.text, True)
        # 将汇总好的分词列表转换为集合（去重）
        father_head_words = set(father_head_words)
        # 转换为词频，如果原标题文本计数器已有该词则覆盖掉（避免标题常见词的词频虚高）
        for word in father_head_words:
            self.head_counter[word] = 1 / len(father_head_words)

    # 添加子标题（子标题为根的子树构建完毕后再执行）
    def add_child(self, child):
        # 添加ID
        self.children.append(child.ID)
        # 合并分词计数器（保留最大计数）
        self.head_counter |= child.head_counter
        self.counter |= child.counter

    # 获取所有子孙节点（返回广度优先排序的结果）
    def all_children(self, node_dict):
        children_list = [node_dict[ID] for ID in self.children]
        search_list = children_list.copy()
        while search_list:
            new_children_list = []
            for node in search_list:
                new_children_list += [node_dict[ID] for ID in node.children]
            children_list += new_children_list
            search_list = new_children_list
        return children_list


# 为法规文本构建各级标题节点
def build_head_node(filename, id_start, top_head, node_dict):
    time_1 = time.perf_counter()

    # 保存当前各级标题节点的队列，并创建顶级节点
    head_list = [HeadNode(id_start, top_head, 0, top_head, '', -1)]
    # 字典（ID→标题节点）
    node_dict[id_start] = head_list[0]
    ID_now = id_start + 1
    # 逐行读取文件
    with open(filename, encoding='utf-8') as f:
        text_lines = f.readlines()
    # 处理每一行文本
    for line in text_lines:
        if line == '\n':  # 跳过空行
            continue
        line = line.replace('　', ' ').strip()  # 替换全角空格并去掉首尾空格
        if line[-1] == '\n':  # 去掉末尾换行符
            line = line[:-1]
        if not line:
            continue
        # 在该行中找标题
        rank, rank_num, start_i, end_i = find_head(line)
        # 该行没有标题，或标题起点不是行首，或出现诸如“（一）”之类的标题字眼但以分号、句号结尾，则将该行内容视为上一标题的正文
        if rank == -1 or start_i > 0 or line[end_i] != ' ' and line[-1] in '；。':
            head_list[-1].add_content(line)
            continue

        # 要添加新标题，说明上一标题直属内容已经完结，可补充正文和祖辈标题文本的分词计数
        head_list[-1].convert_counter(head_list[:-1])
        # 标题队列中层级低于或等于新标题的，说明已全部完结，可将其添加至父标题
        while rank <= head_list[-1].rank:
            head_list[-2].add_child(head_list.pop())  # head_list[-2]是在pop之前

        head = line[start_i:end_i]  # 取标题字眼
        while line[end_i] == ' ':  # 跳过”章“、”条“等标题字眼后面的空格
            end_i += 1
        if line[-1] in '：；。！？…':  # 以标点符号结尾的，视为正文，否则视为标题文本
            text, content = '', line[end_i:]
        else:
            text, content = line[end_i:], ''
        # 创建新标题节点
        head_list.append(HeadNode(ID_now, head, rank, text, content, head_list[-1].ID))
        node_dict[ID_now] = head_list[-1]
        ID_now += 1

    # 原始文本已读取完毕，将最后的标题节点补充正文和祖辈标题文本的分词计数
    head_list[-1].convert_counter(head_list[:-1])
    # 队列中的标题全部添加至父标题
    while len(head_list) > 1:
        head_list[-2].add_child(head_list.pop())

    time_2 = time.perf_counter()
    print('%s包含各级标题%d个，读取耗时%.2f ms' % (top_head, ID_now - id_start, (time_2 - time_1) * 1000))


def test_qa(namelist):
    # 有序字典（ID→标题节点）
    head_dict = collections.OrderedDict()
    # 代表各法规的顶级节点的ID列表
    topID_list = []
    # 读取每一部法规
    for filename in namelist:
        topID_list.append(len(head_dict))
        build_head_node(filename + '.txt', len(head_dict), filename, head_dict)
    # 计算各分词的全局IDF值（提供全部叶节点）
    time_1 = time.perf_counter()
    leaf_nodes = [node for node in head_dict.values() if not node.children]
    calculate_idf(leaf_nodes)
    time_2 = time.perf_counter()
    print('计算%d个分词在%d个叶节点的IDF值，耗时%.2f ms' % (len(word_set), len(leaf_nodes), (time_2 - time_1) * 1000))

    while True:
        question = input('请提问：')

        problem = 0  # 题目的类型，0表示不是题目
        option_dict = {}  # 选择题的选项字典（字母→文本）
        if question.startswith('[判断]'):
            problem = 1
        elif question.startswith('[单选]'):
            problem = 2
        elif question.startswith('[多选]'):
            problem = 3
        elif question.startswith('[填空]'):
            problem = 4
        if problem:  # 如果是题目，去掉开头的标志
            question = question[4:].strip()
            if problem in (2, 3):  # 选择题读取选项，问题和各选项之间以空格分割，如“xxx（ ）xxx A.xx B.xx C.xx”
                question, *options = question.replace('（ ）', '（）').split(' ')  # 把空位括号中间的空格去掉，再按空格切分
                option_dict = dict(option.split('.') for option in options if option)

        # 将问题分词并去掉停用词
        q_words = word_cut(question)
        # 无意义的问题
        if not q_words:
            print('无意义的问题')
            continue

        if question[-1] in '?？。':  # 去掉末尾问号、句号
            question = question[:-1]

        # 匹配的标题节点列表（答案列表）
        answer_nodes = []
        # 以顶级节点构成初始搜索列表
        search_list = [head_dict[ID] for ID in topID_list]

        # 在问题中依次找各级标题（是题目则跳过这一步）
        q_index = 0  # 待匹配问题字段的起始位置
        while search_list and q_index < len(question) and not problem:
            # 尝试以问题字段匹配待搜索节点的标题字眼
            match_nodes = [node for node in search_list if question[q_index:].startswith(node.head)]
            match_len = len(match_nodes[0].head) if match_nodes else 0
            # 标题字眼未匹配上，尝试用待搜索节点的标题文本匹配
            if not match_nodes:
                match_nodes = [node for node in search_list if node.text and question[q_index:].startswith(node.text)]
                match_len = len(match_nodes[0].text) if match_nodes else 0
            # 仍未匹配上，检查问题字段是不是标题字眼
            if not match_nodes:
                rank, rank_num, start_i, end_i = find_head(question[q_index:])
                if rank != -1 and start_i == 0:
                    # 问题字段是标题字眼，尝试匹配后代节点的标题字眼
                    for node in search_list:
                        match_nodes += [child for child in node.all_children(head_dict)
                                        if question[q_index:].startswith(child.head)]
                    match_len = end_i

            if match_nodes:  # 有匹配节点
                answer_nodes = match_nodes  # 将匹配节点放入答案列表
                search_list.clear()
                # 将匹配节点的下级放入下一轮搜索列表
                for node in match_nodes:
                    search_list += [head_dict[ID] for ID in node.children]
                # 将问题待匹配的位置向前推进（标题字眼或标题文本的长度）
                q_index += match_len
            else:  # 未匹配上则跳出循环，进入下一步搜索
                break

        show_whole = False  # 显示完整子节点信息
        q_end = question[q_index:]  # 未匹配的问题字段
        if not problem:
            if q_end == '':
                # 问题已被各级标题完整匹配，无需下一步搜索
                search_list = []
            else:
                # 用同义词替换问句用词，减少枚举量
                q_end = q_end.replace('的', '').replace('了', '').replace('是', '有') \
                    .replace('讲', '有').replace('说', '有').replace('包括', '有').replace('包含', '有') \
                    .replace('含有', '有').replace('有有', '有').replace('什么', '啥').replace('哪些', '啥') \
                    .replace('怎么', '啥').replace('如何', '啥').replace('有何', '啥').replace('章节', '内容') \
                    .replace('条款', '内容').replace('条目', '内容').replace('全部', '完整').replace('所有', '完整')
                if '完整内容' in q_end or '完整规定' in q_end or '全文' in q_end:
                    # 出现了“完整”等字眼，输出结果要显示完整的子节点内容
                    show_whole = True
                    q_end = q_end.replace('完整', '').replace('全文', '')
                if q_index and q_end in ('', '有', '有啥', '啥有', '内容', '规定' '啥内容', '啥规定', '有内容', '有规定',
                                         '规定啥', '内容啥', '有啥内容', '有啥规定', '规定内容', '规定啥内容'):
                    # 问题至少以一个标题字眼（或标题文本）开头，且未匹配的部分是问词，也无需下一步搜索
                    search_list = []
                else:
                    end_words = [word for word in ('有', '有啥', '内容', '规定' '啥内容', '啥规定', '有内容', '有规定',
                                                   '规定啥', '有啥内容', '有啥规定', '规定内容', '规定啥内容')
                                 if q_end.endswith(word)]
                    if end_words:  # 以问词结尾，则去掉问词（按最长的长度去掉）
                        q_end = q_end[:-max(map(len, end_words))]

        unfold_list = set()  # 已展开的节点（防止重复展开）
        # 搜索TF-IDF值较高的节点
        while search_list:
            # 检查需要匹配的问题字段（或去掉问词的问题字段）是否与待搜索节点的标题文本一致
            answer_nodes = [node for node in search_list if node.pure_text() in (question[q_index:], q_end)]
            if answer_nodes:
                break
            # 将TF-IDF值和对应节点绑定并按TF-IDF值降序排列
            tfidf_nodes = sorted(zip(get_tfidf(q_words, search_list), search_list), key=itemgetter(0), reverse=True)
            # 打印搜索过程中各节点的TF-IDF值
            # for tfidf, node in tfidf_nodes:
            #     print('%s(%s):%d ' % (node.head, node.text, round(tfidf * 10000)), end=' ')
            # print()
            # 将搜索列表和答案列表清零
            search_list.clear()
            # answer_nodes.clear()
            # 计算TF-IDF值门槛（须同时大于最大值的(询问时MATCH_ACCU/做题时PROBLEM_ACCU)和TFIDF_MIN）
            tfidf_limit = max(tfidf_nodes[0][0] * (PROBLEM_ACCU if problem else MATCH_ACCU), TFIDF_MIN)
            # 筛选TF-IDF值大于门槛的标题节点，将其子标题放入搜索列表，没有子标题的节点放入答案列表
            for tfidf, node in tfidf_nodes:
                if tfidf < tfidf_limit:
                    break
                if node.children and node not in unfold_list:  # 有子标题，且未搜索过
                    search_list += [head_dict[ID] for ID in node.children]
                    unfold_list.add(node)
                if node.content:  # 有正文可加入答案列表
                    answer_nodes.append(node)
            # 如果搜索列表不为空，则将答案列表中的节点加入搜索列表参与下一轮比较
            if search_list:
                search_list += answer_nodes

        if not answer_nodes:
            print('未找到匹配的条款')
            continue

        # 尝试给出题目答案
        if problem:
            if problem == 1:  # 判断
                # 在匹配标题节点的正文中找原话
                for node in answer_nodes:
                    if question in node.content:
                        print('参考答案：正确')
                        answer_nodes = [node]
                        break
                else:  # 未找到原话
                    print('参考答案：错误')
            if problem == 2:  # 单选
                for node in answer_nodes:
                    # 以原文中出现的选项的序号构成答案列表
                    answer_list = [ans for ans, opt in option_dict.items() if opt in node.content]
                    if len(answer_list) == 1:  # 仅有一个选项，则找到答案
                        print('参考答案：' + answer_list[0])
                        answer_nodes = [node]
                        break
                    elif len(answer_list) > 1:  # 大于一个选项，尝试其他方法
                        if question.count('（）') == 1:  # 问题包含一个空位，则尝试用选项替换空位再匹配原文
                            answer_list = [ans for ans in answer_list
                                           if question.replace('（）', option_dict[ans]) in node.content]
                            if len(answer_list) == 1:  # 仅有一个选项，则找到答案
                                print('参考答案：' + answer_list[0])
                                answer_nodes = [node]
                                break
                        elif question in node.content:  # 问题不包含空位，且在正文出现，则计算选项与问题的距离
                            q_beg = node.content.find(question)
                            q_end = q_beg + len(question)
                            opt_dis = []
                            for ans in answer_list:
                                opt = option_dict[ans]
                                left_pos = node.content[:q_beg].rfind(opt)  # 选项在问题左边的位置
                                right_pos = node.content[q_end:].find(opt)  # 选项在问题右边的位置
                                if max(left_pos, right_pos) == -1:  # 选项在问题两边都没有出现，则出现在问题中间
                                    opt_dis.append(0)  # 这种情况比较特殊
                                elif min(left_pos, right_pos) != -1:  # 选项在问题两边都出现了，则选择距离小的一个
                                    opt_dis.append(min(q_beg - left_pos - len(opt), right_pos))
                                else:  # 选项只在问题一边出现
                                    opt_dis.append(q_beg - left_pos - len(opt) if left_pos != -1 else right_pos)
                            if opt_dis.count(min(opt_dis)) == 1:  # 仅有唯一的距问题最近的选项，则以它作为答案
                                print('参考答案：' + answer_list[opt_dis.index(min(opt_dis))])
                                answer_nodes = [node]
                                break
                    elif question.count('（）') == list(option_dict.values())[0].count('；') + 1:
                        ans_found = False
                        # 问题包含多个空位，则用选项中的多个词（以“；”分割，数量与空位相同）依次替换空位再尝试匹配原文
                        for ans, opt in option_dict.items():
                            q_rep = question
                            for word in opt.split('；'):  # 选项中的多个词依次替换空位
                                q_rep = q_rep.replace('（）', word, 1)
                            if q_rep in node.content:
                                answer_nodes = [node]
                                print('参考答案：' + ans)
                                ans_found = True
                                break
                        if ans_found:
                            break
                else:
                    print('未能找到匹配的原文')
            if problem == 3:  # 多选
                for node in answer_nodes:
                    # 以原文中出现的选项的序号构成答案列表
                    answer_list = [ans for ans, opt in option_dict.items() if opt in node.content]
                    if len(answer_list) > 1:  # 多选的结果数大于1
                        if question.count('（）') in (0, len(answer_list)):  # 没有空位，或空位数等于找到的选项数，则确定为答案
                            print('参考答案：' + ''.join(sorted(answer_list)))
                            answer_nodes = [node]
                            break
                        elif 1 < question.count('（）') < len(answer_list):
                            # 将问题以空位分成几段
                            q_segments = question.split('（）')
                            # 保证q_segments数量比空位多1
                            if question.startswith('（）'):  # 问题以空位起始
                                q_segments.insert(0, '')  # 第一个空位前补空串
                            if question.endswith('（）'):  # 问题以空位结尾
                                q_segments.append('')  # 最后一个空位后补空串
                            # 避免把第一个空位所在句子的前一句话纳入进来
                            pos1 = q_segments[0].rfind('：')
                            pos2 = q_segments[0].rfind('，')
                            pos3 = q_segments[0].rfind('。')
                            latest_pos = max(pos1, pos2, pos3) + 1
                            if latest_pos:
                                q_segments[0] = q_segments[0][latest_pos:]
                            # 逐个空位找答案
                            answer_list.clear()
                            for i in range(question.count('（）')):  # 依次用各选项替换空位并拼接前后文字，尝试匹配
                                answer_list += [ans for ans, opt in option_dict.items()
                                                if q_segments[i] + opt + q_segments[i + 1] in node.content]
                            if question.count('（）') == len(answer_list):
                                print('参考答案：' + ''.join(answer_list))
                                answer_nodes = [node]
                                break
                else:
                    print('未能找到匹配的原文')
            if problem == 4:  # 填空
                # 将问题以空位分成几段
                q_segments = question.split('（ ）')
                for node in answer_nodes:
                    seg_pos = []  # 各分段的端点在原文中的位置
                    start_pos = 0  # 寻找分段的起始位置
                    for seg in q_segments:
                        seg_beg = node.content.find(seg, start_pos)
                        if seg_beg == -1:  # 原文没找到该段
                            break
                        start_pos = seg_beg + len(seg)  # 从本段末尾寻找下一分段
                        seg_pos.extend([seg_beg, start_pos])

                    if len(seg_pos) != len(q_segments) * 2:  # 未找到所有分段
                        continue  # 换下一节点接着找

                    # 已找到所有分段，检查是否有空位在起始或末尾位置
                    if question.startswith('（ ）'):  # 问题以空位起始
                        # 避免把前一句话或序号纳入进来
                        pos1 = node.content[:seg_pos[0]].rfind('。')
                        pos2 = node.content[:seg_pos[0]].rfind('；')
                        pos3 = node.content[:seg_pos[0]].rfind('）')
                        pos4 = node.content[:seg_pos[0]].rfind('.')
                        latest_pos = max(pos1, pos2, pos3, pos4) + 1  # 都没找到，则为-1+1=0；找到了，则把最近的符号跳过去
                        # 补充第一个空位答案的起始位置
                        seg_pos.insert(0, latest_pos)
                    else:  # 如果问题不以空位起始，去掉第一个分段的起始位置
                        seg_pos.pop(0)
                    if question.endswith('（ ）'):  # 问题以空位结尾
                        # 避免把后一句话纳入进来
                        pos1 = node.content.find('。', start_pos)
                        pos2 = node.content.find('；', start_pos)
                        # 补充最后一个空位答案的末尾位置
                        if max(pos1, pos2) == -1:  # 都没找到，以正文末尾作为最后一个答案的末尾
                            seg_pos.append(len(node.content))
                        elif min(pos1, pos2) > -1:  # 都找到了，以最近一个符号位置作为最后一个答案的末尾
                            seg_pos.append(min(pos1, pos2))
                        else:  # 只找到一个符号，以该符号位置作为最后一个答案的末尾
                            seg_pos.append(max(pos1, pos2))
                    else:  # 如果问题不以空位结束，去掉最后一个分段的末尾位置
                        seg_pos.pop()
                    # 取出答案
                    print('参考答案：' + '；'.join(node.content[seg_pos[i]:seg_pos[i + 1]]
                                             for i in range(0, len(seg_pos), 2)))
                    answer_nodes = [node]
                    break
                else:
                    print('未能找到匹配的原文')

        display_IDs = set()  # 已经显示的节点的ID号
        # 回答问题
        for node in answer_nodes:
            # print(node.head_counter)
            # print(node.counter)
            if node.children:
                # 如果不是叶节点，要回答下级标题信息
                if display_IDs & set(node.children):  # 已显示该节点的子标题，跳过该节点
                    continue
                res = node.content + '\n包含' if node.content else '包含'  # 有正文先回答正文
                for child in [head_dict[ID] for ID in node.children]:
                    if child.head[-1] in ('.', '）'):
                        # 如果标题字眼是“（一）”、“1.”之类，只显示标题文本
                        head = child.text
                    else:
                        head = child.head
                        if child.text:
                            # 如果有标题文本，用括号包裹
                            head += '（%s）' % child.text
                    res += head + '、'
                res = res[:-1] + '。'  # 替换末尾的“、”
                all_children = node.all_children(head_dict)  # 全部子标题
                item_list = list(filter(lambda x: '条' in x.head, all_children))  # 筛选其中的“条”
                if item_list and head_dict[node.children[0]].rank < item_list[0].rank:
                    # 如果节点下面有“条”且不是直接下级，显示条的数目
                    res = res[:-1] + '，共计%d条。' % len(item_list)
                if show_whole:  # 显示完整子节点信息
                    all_children.sort(key=lambda x: x.ID)  # 各标题按深度优先排序（在原文中出现的顺序）
                    for child in all_children:
                        res += '\n' + child.head
                        if child.head[-1] not in ('.', '）') and child.text:
                            # 标题字眼和标题文本之间加一个空格（“（一）”、“1.”字眼之后不加空格）
                            res += ' '
                        res += child.text
                        if child.content:
                            res += '\n' + child.content
            else:
                if not problem:
                    # 如果问题的最后一个分词（如“xxx的要领”中的“要领”）在正文中有单独解释（如“要领：xxx”），则单独输出这一解释
                    # 待匹配的问句以标题文本开头（问条款的某一点）
                    pure_text = node.pure_text()  # 去掉末尾括号（若有）的标题文本
                    if pure_text and question[q_index:].startswith(pure_text):
                        q_item = question[q_index + len(pure_text):].replace(' ', '')  # 被询问的项目
                        while q_item and q_item[0] in '的':  # 去掉“的”
                            q_item = q_item[1:]
                        # 被询问的项目不是停用词，且在正文中只出现一次（避免歧义）
                        if len(q_item) > 1 and q_item not in stop_words and node.content.count(q_item) == 1:
                            beg_pos = node.content.find(q_item)  # 关键词的起始位置
                            start_pos = beg_pos + len(q_item)  # 关键词的结束位置（对其解释的起始位置）
                            # 避免把后一句话纳入进来，寻找解释的结束位置
                            pos1 = node.content.find('。', start_pos)  # 句号位置
                            if pos1 == -1:  # 未找到句号（这种情况不正常），到正文结尾
                                pos1 = len(node.content)
                            pos2 = node.content.find('；', start_pos)  # 分号优先的结束位置
                            if pos2 == -1 or pos2 > pos1:  # 找不到分号或分号在句号后面
                                pos2 = pos1
                            pos3 = node.content.find('，', start_pos)  # 逗号优先的结束位置
                            if pos3 == -1 or pos3 > pos2:  # 找不到逗号或逗号在分号后面
                                pos3 = pos2
                            # 根据关键词前后标点符号决定解释的结束位置
                            if node.content[start_pos] in '：':  # 关键词后跟“：”，优先以句号结束
                                start_pos += 1  # 跳过“：”
                                end_pos = pos1
                            elif beg_pos != 0 and node.content[beg_pos - 1] in '，':  # 关键词前是“，”，优先以逗号结束
                                end_pos = pos3
                            else:  # 默认以分号结束
                                end_pos = pos2
                            explan = node.content[start_pos:end_pos]  # 取出解释
                            if explan and '：' not in explan:
                                print(explan)
                # 叶节点以正文作为回答
                res = node.content

            # 拼接信息来源
            source = ''
            # 从当前标题节点上溯至顶级节点
            source_node = node
            while source_node.father_ID in head_dict:
                head = source_node.head
                if head[-1] not in ('.', '）') and source_node.text:
                    # 标题字眼和标题文本之间加一个空格（“（一）”、“1.”字眼之后不加空格）
                    head += ' '
                head += source_node.text
                source = head + '/' + source
                source_node = head_dict[source_node.father_ID]
            # 至此source_node已是顶级节点，如果node就是顶级节点，则不会进入上面的while循环，source = ''
            source = source_node.head + '/' + source[:-1] if source else source_node.head
            # 打印该节点的参考信息
            print('%s：\n%s' % (source, res))
            display_IDs.add(node.ID)


if __name__ == '__main__':
    test_qa(['城市道路管理条例',
             '中华人民共和国道路运输条例',
             '中华人民共和国道路交通安全法实施条例',
             '收费公路管理条例',
             '公路安全保护条例'])
